// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::collections::{TryReserveError, hash_map::Entry};

use crate::{
    ecmascript::{
        abstract_operations::{
            operations_on_objects::{construct, get, get_function_realm},
            testing_and_comparison::{is_array, is_constructor, same_value},
            type_conversion::{to_number, to_uint32, to_uint32_number},
        },
        builtins::{
            ArgumentsList,
            array::{Array, ArrayHeapData},
        },
        execution::{
            Agent, JsResult,
            agent::{ExceptionType, TryError, TryResult},
        },
        types::{
            BUILTIN_STRING_MEMORY, IntoObject, Number, Object, OrdinaryObject, PropertyDescriptor,
            Value,
        },
    },
    engine::{
        context::{Bindable, GcScope, NoGcScope},
        rootable::Scopable,
    },
    heap::{CreateHeapData, Heap, WellKnownSymbolIndexes, element_array::ElementStorageMut},
};

/// ### [10.4.2.2 ArrayCreate ( length \[ , proto \] )](https://tc39.es/ecma262/#sec-arraycreate)
///
/// The abstract operation ArrayCreate takes argument length (a non-negative
/// integer) and optional argument proto (an Object) and returns either a
/// normal completion containing an Array exotic object or a throw completion.
/// It is used to specify the creation of new Arrays.
pub(crate) fn array_create<'a>(
    agent: &mut Agent,
    length: usize,
    capacity: usize,
    proto: Option<Object>,
    gc: NoGcScope<'a, '_>,
) -> JsResult<'a, Array<'a>> {
    // 1. If length > 2**32 - 1, throw a RangeError exception.
    if length > (2usize.pow(32) - 1) {
        return Err(agent.throw_exception_with_static_message(
            ExceptionType::RangeError,
            "invalid array length",
            gc,
        ));
    }
    // 2. If proto is not present, set proto to %Array.prototype%.
    let object_index = if let Some(proto) = proto {
        if proto
            == agent
                .current_realm_record()
                .intrinsics()
                .array_prototype()
                .into_object()
        {
            None
        } else {
            Some(
                OrdinaryObject::create_object(agent, Some(proto), &[])
                    .expect("Should perform GC here")
                    .bind(gc),
            )
        }
    } else {
        None
    };
    // 3. Let A be MakeBasicObject(« [[Prototype]], [[Extensible]] »).
    // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1.
    let mut elements = match agent.heap.elements.allocate_elements_with_length(capacity) {
        Ok(e) => e,
        Err(err) => {
            return Err(agent.throw_allocation_exception(err, gc));
        }
    };
    elements.len = length as u32;
    let data = ArrayHeapData {
        // 4. Set A.[[Prototype]] to proto.
        object_index,
        elements: elements.bind(gc),
    };

    // 7. Return A.
    Ok(agent.heap.create(data))
}

/// ### [10.4.2.3 ArraySpeciesCreate ( originalArray, length )](https://tc39.es/ecma262/#sec-arrayspeciescreate)
///
/// The abstract operation ArraySpeciesCreate takes arguments originalArray (an
/// Object) and length (a non-negative integer) and returns either a normal
/// completion containing an Object or a throw completion. It is used to
/// specify the creation of a new Array or similar object using a constructor
/// function that is derived from originalArray. It does not enforce that the
/// constructor function returns an Array.
///
/// > Note: If originalArray was created using the standard built-in Array
/// > constructor for a realm that is not the realm of the running execution
/// > context, then a new Array is created using the realm of the running
/// > execution context. This maintains compatibility with Web browsers that
/// > have historically had that behaviour for the Array.prototype methods
/// > that now are defined using ArraySpeciesCreate.
pub(crate) fn array_species_create<'a>(
    agent: &mut Agent,
    original_array: Object,
    length: usize,
    mut gc: GcScope<'a, '_>,
) -> JsResult<'a, Object<'a>> {
    let nogc = gc.nogc();
    let original_array = original_array.bind(nogc);
    // 1. Let isArray be ? IsArray(originalArray).
    let original_is_array = is_array(agent, original_array, nogc).unbind()?;
    // 2. If isArray is false, return ? ArrayCreate(length).
    if !original_is_array {
        let new_array = array_create(agent, length, length, None, gc.into_nogc())?;
        return Ok(new_array.into_object());
    }
    // 3. Let C be ? Get(originalArray, "constructor").
    let mut c = get(
        agent,
        original_array.unbind(),
        BUILTIN_STRING_MEMORY.constructor.into(),
        gc.reborrow(),
    )
    .unbind()?
    .bind(gc.nogc());
    // 4. If IsConstructor(C) is true, then
    if let Some(c_func) = is_constructor(agent, c) {
        // a. Let thisRealm be the current Realm Record.
        let this_realm = agent.current_realm(gc.nogc());
        // b. Let realmC be ? GetFunctionRealm(C).
        let realm_c = get_function_realm(agent, c_func, gc.nogc())
            .unbind()?
            .bind(gc.nogc());
        // c. If thisRealm and realmC are not the same Realm Record, then
        if this_realm != realm_c {
            // i. If SameValue(C, realmC.[[Intrinsics]].[[%Array%]]) is true, set C to undefined.
            if same_value(
                agent,
                c,
                agent.get_realm_record_by_id(realm_c).intrinsics().array(),
            ) {
                c = Value::Undefined;
            }
        }
    }
    // 5. If C is an Object, then
    if let Ok(c_obj) = Object::try_from(c) {
        // a. Set C to ? Get(C, @@species).
        c = get(
            agent,
            c_obj.unbind(),
            WellKnownSymbolIndexes::Species.into(),
            gc.reborrow(),
        )
        .unbind()?
        .bind(gc.nogc());
        // b. If C is null, set C to undefined.
        if c.is_null() {
            c = Value::Undefined;
        }
    }
    // 6. If C is undefined, return ? ArrayCreate(length).
    if c.is_undefined() {
        let new_array = array_create(agent, length, length, None, gc.into_nogc())?;
        return Ok(new_array.into_object());
    }
    // 7. If IsConstructor(C) is false, throw a TypeError exception.
    let Some(c) = is_constructor(agent, c) else {
        return Err(agent.throw_exception_with_static_message(
            ExceptionType::TypeError,
            "Not a constructor",
            gc.into_nogc(),
        ));
    };
    // 8. Return ? Construct(C, « 𝔽(length) »).
    let length = Value::from_f64(agent, length as f64, gc.nogc());
    construct(
        agent,
        c.unbind(),
        Some(ArgumentsList::from_mut_value(&mut length.unbind())),
        None,
        gc,
    )
}

/// ### [10.4.2.4 ArraySetLength ( A, Desc )](https://tc39.es/ecma262/#sec-arraysetlength)
///
/// The abstract operation ArraySetLength takes arguments A (an Array) and Desc (a Property Descriptor) and returns either a normal completion containing a Boolean or a throw completion.
pub(crate) fn array_set_length<'a>(
    agent: &mut Agent,
    a: Array,
    desc: PropertyDescriptor,
    mut gc: GcScope<'a, '_>,
) -> JsResult<'a, bool> {
    let a = a.bind(gc.nogc());
    let desc = desc.bind(gc.nogc());
    // 1. If Desc does not have a [[Value]] field, then
    let Some(desc_value) = desc.value else {
        // a. Return ! OrdinaryDefineOwnProperty(A, "length", Desc).
        return Ok(array_set_length_no_value_field(agent, a, desc));
    };
    // 2. Let newLenDesc be a copy of Desc.
    let PropertyDescriptor {
        enumerable: desc_enumerable,
        configurable: desc_configurable,
        writable: desc_writable,
        ..
    } = desc;

    // 3. Let newLen be ? ToUint32(Desc.[[Value]]).
    let a = a.scope(agent, gc.nogc());
    let scoped_desc_value = desc_value.scope(agent, gc.nogc());
    let new_len = to_uint32(agent, desc_value.unbind(), gc.reborrow()).unbind()?;
    // 4. Let numberLen be ? ToNumber(Desc.[[Value]]).
    let number_len = to_number(
        agent,
        // SAFETY: scoped_desc_value is not shared.
        unsafe { scoped_desc_value.take(agent) },
        gc.reborrow(),
    )
    .unbind()?
    .bind(gc.nogc());
    // 5. If SameValueZero(newLen, numberLen) is false, throw a RangeError exception.
    if !Number::same_value_zero(agent, number_len, new_len.into()) {
        return Err(agent.throw_exception_with_static_message(
            ExceptionType::RangeError,
            "invalid array length",
            gc.into_nogc(),
        ));
    }
    let gc = gc.into_nogc();
    let a = a.get(agent).bind(gc);
    // 6. Set newLenDesc.[[Value]] to newLen.
    // 7. Let oldLenDesc be OrdinaryGetOwnProperty(A, "length").
    array_set_length_handling(
        agent,
        a,
        new_len,
        desc_configurable,
        desc_enumerable,
        desc_writable,
    )
    .map_err(|err| agent.throw_allocation_exception(err, gc))
}

pub(crate) fn array_try_set_length<'gc>(
    agent: &mut Agent,
    a: Array,
    desc: PropertyDescriptor,
    gc: NoGcScope<'gc, '_>,
) -> TryResult<'gc, bool> {
    // 1. If Desc does not have a [[Value]] field, then
    let Some(desc_value) = desc.value else {
        // a. Return ! OrdinaryDefineOwnProperty(A, "length", Desc).
        return TryResult::Continue(array_set_length_no_value_field(agent, a, desc));
    };
    // 2. Let newLenDesc be a copy of Desc.
    // 3. Let newLen be ? ToUint32(Desc.[[Value]]).
    // 4. Let numberLen be ? ToNumber(Desc.[[Value]]).
    let Ok(number_len) = Number::try_from(desc_value) else {
        return TryError::GcError.into();
    };
    let new_len = to_uint32_number(agent, number_len);
    // 5. If SameValueZero(newLen, numberLen) is false, throw a RangeError exception.
    if !Number::same_value_zero(agent, number_len, new_len.into()) {
        return TryError::GcError.into();
    }
    // 6. Set newLenDesc.[[Value]] to newLen.
    // 7. Let oldLenDesc be OrdinaryGetOwnProperty(A, "length").
    match array_set_length_handling(
        agent,
        a,
        new_len,
        desc.configurable,
        desc.enumerable,
        desc.writable,
    ) {
        Ok(b) => TryResult::Continue(b),
        Err(err) => agent.throw_allocation_exception(err, gc).into(),
    }
}

fn array_set_length_no_value_field(agent: &mut Agent, a: Array, desc: PropertyDescriptor) -> bool {
    // a. Return ! OrdinaryDefineOwnProperty(A, "length", Desc).
    if !desc.has_fields() {
        return true;
    }
    if desc.configurable == Some(true) || desc.enumerable == Some(true) {
        return false;
    }
    if !desc.is_generic_descriptor() && desc.is_accessor_descriptor() {
        return false;
    }
    if !a.length_writable(agent) {
        // Length is already frozen.
        if desc.writable == Some(true) {
            return false;
        }
    } else if desc.writable == Some(false) {
        a.set_length_readonly(agent);
    }
    true
}

pub(crate) fn array_set_length_handling(
    agent: &mut Agent,
    a: Array,
    new_len: u32,
    desc_configurable: Option<bool>,
    desc_enumerable: Option<bool>,
    desc_writable: Option<bool>,
) -> Result<bool, TryReserveError> {
    // 6. Set newLenDesc.[[Value]] to newLen.
    // 7. Let oldLenDesc be OrdinaryGetOwnProperty(A, "length").
    let Heap {
        arrays, elements, ..
    } = &mut agent.heap;
    let elems = a.get_elements_mut(arrays);
    // 8. Assert: oldLenDesc is not undefined.
    // 9. Assert: IsDataDescriptor(oldLenDesc) is true.
    // 10. Assert: oldLenDesc.[[Configurable]] is false.
    // 11. Let oldLen be oldLenDesc.[[Value]].
    let (old_len, old_len_writable) = (elems.len(), elems.len_writable);
    // 12. If newLen ≥ oldLen, then
    if new_len >= old_len {
        // a. Return ! OrdinaryDefineOwnProperty(A, "length", newLenDesc).
        if !array_ordinary_define_property_checks(
            old_len_writable,
            old_len,
            desc_configurable,
            desc_enumerable,
            desc_writable,
            new_len,
        ) {
            return Ok(false);
        }
        elems.reserve(elements, new_len)?;
        elems.len = new_len;
        elems.len_writable = desc_writable.unwrap_or(old_len_writable);
        return Ok(true);
    }
    debug_assert!(old_len > new_len);
    // 13. If oldLenDesc.[[Writable]] is false, return false.
    if !old_len_writable {
        return Ok(false);
    }
    // 14. If newLenDesc does not have a [[Writable]] field or
    //     newLenDesc.[[Writable]] is true, then
    //     a. Let newWritable be true.
    // 15. Else,
    // a. NOTE: Setting the [[Writable]] attribute to false is deferred in case
    //    any elements cannot be deleted.
    // b. Let newWritable be false.
    // c. Set newLenDesc.[[Writable]] to true.
    let new_writable = desc_writable.unwrap_or(true);
    // 16. Let succeeded be
    //     ! OrdinaryDefineOwnProperty(A, "length", newLenDesc).
    // 17. If succeeded is false, return false.
    if !array_ordinary_define_property_checks(
        old_len_writable,
        old_len,
        desc_configurable,
        desc_enumerable,
        if desc_writable == Some(false) {
            Some(true)
        } else {
            desc_writable
        },
        new_len,
    ) {
        return Ok(false);
    }
    let ElementStorageMut {
        values,
        descriptors,
    } = elements.get_element_storage_mut(elems);
    if let Entry::Occupied(mut descriptors) = descriptors
        && !descriptors.get().is_empty()
    {
        let descs = descriptors.get_mut();
        // 17. For each own property key P of A such that P is an array index
        //     and ! ToUint32(P) ≥ newLen, in descending numeric index order, do
        for i in (new_len..old_len).rev() {
            // a. Let deleteSucceeded be ! A.[[Delete]](P).
            // Note: we skip the delete and only perform it at the end.
            let delete_succeeded = if let Some(d) = descs.get(&i) {
                if d.is_configurable() {
                    descs.remove(&i);
                    true
                } else {
                    // Unconfigurable entry, can't delete.
                    false
                }
            } else {
                true
            };
            // b. If deleteSucceeded is false, then
            if !delete_succeeded {
                // Delete properties that didn't fail.
                values[(i + 1) as usize..old_len as usize].fill(None);
                // i. Set newLenDesc.[[Value]] to ! ToUint32(P) + 1𝔽.
                elems.len = i + 1;
                // ii. If newWritable is false, set newLenDesc.[[Writable]] to false.
                elems.len_writable &= new_writable;
                // iii. Perform ! OrdinaryDefineOwnProperty(A, "length", newLenDesc).
                // iv. Return false.
                return Ok(false);
            }
        }
        if descs.is_empty() {
            descriptors.remove();
        }
    }
    // No descriptors in the delete range: all entries are normal values.
    values[new_len as usize..old_len as usize].fill(None);
    elems.len = new_len;
    // 18. If newWritable is false, then
    elems.len_writable &= new_writable;
    // a. Set succeeded to ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Writable]]: false }).
    // b. Assert: succeeded is true.
    // 19. Return true.
    Ok(true)
}

fn array_ordinary_define_property_checks(
    current_writable: bool,
    current_value: u32,
    desc_configurable: Option<bool>,
    desc_enumerable: Option<bool>,
    desc_writable: Option<bool>,
    desc_value: u32,
) -> bool {
    // a. If Desc has a [[Configurable]] field and Desc.[[Configurable]] is
    //    true, return false.
    if desc_configurable == Some(true) {
        return false;
    }
    // b. If Desc has an [[Enumerable]] field and Desc.[[Enumerable]] is
    //    not current.[[Enumerable]], return false.
    if desc_enumerable == Some(true) {
        return false;
    }
    // e. Else if current.[[Writable]] is false, then
    if !current_writable {
        // i. If Desc has a [[Writable]] field and Desc.[[Writable]] is
        //    true, return false.
        if desc_writable == Some(true) {
            return false;
        }
        // ii. NOTE: SameValue returns true for NaN values which may be
        //     distinguishable by other means. Returning here ensures that
        //     any existing property of O remains unmodified.
        // iii. If Desc has a [[Value]] field, return
        //      SameValue(Desc.[[Value]], current.[[Value]]).
        if desc_value != current_value {
            return false;
        }
    }
    true
}
